package com.ruoyi.framework.mybatisplus.interceptor;

import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.ruoyi.common.core.text.StrFormatter;
import com.ruoyi.common.exception.NotFountDeleteFiledException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.framework.mybatisplus.util.MysqlTableFieldCacheUtils;
import lombok.Data;
import lombok.EqualsAndHashCode;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.statement.update.UpdateSet;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

import java.sql.Connection;
import java.util.ArrayList;
import java.util.stream.Collectors;

/**
 * @author yql
 * @describe ControllerSqlInterceptor
 * @date 2023/3/8 17:53
 */
@EqualsAndHashCode(callSuper = true)
@Data
@Component
@ConditionalOnProperty(value = {"mybatis-plus.global-config.db-config.logic-delete-field", "mybatis-plus.global-config.db-config.logic-not-delete-value", "mybatis-plus.global-config.db-config.logic-delete-value"})
public class LogicDeleteInterceptor extends JsqlParserSupport implements InnerInterceptor {

    private static Logger log = LoggerFactory.getLogger(LogicDeleteInterceptor.class);
    @Value("${mybatis-plus.global-config.db-config.logic-delete-field}")
    private String fieldName;


    @Value("${mybatis-plus.global-config.db-config.logic-delete-value}")
    private Long deleteValue;

    @Value("${mybatis-plus.global-config.db-config.logic-not-delete-value}")
    private Long notDeleteValue;

    public LogicDeleteInterceptor(Class clazz) {
        // TODO document why this constructor is empty
    }

    public LogicDeleteInterceptor() {

    }

    @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
        MappedStatement ms = mpSh.mappedStatement();
        PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
        mpBs.sql(parserSingle(mpBs.sql(), null));
    }

    @Override
    public String parserSingle(String sql, Object obj) {
        try {
            Statement statement = CCJSqlParserUtil.parse(sql);
            return processParser(statement, 0, sql, obj);
        } catch (JSQLParserException e) {
            return sql;
        }
    }

    @Override
    protected String processParser(Statement statement, int index, String sql, Object obj) {
        String updateSql = null;
        try {
            if (statement instanceof Insert) {
                selectAppendDeleteField(((Insert) statement).getSelect());
            } else if (statement instanceof Select) {
                selectAppendDeleteField((Select) statement);
            } else if (statement instanceof Update) {
                updateAppendDeleteField((Update) statement);
            } else if (statement instanceof Delete) {
                updateSql = deleteSqlToUpdate((Delete) statement);
            }
        } catch (NotFountDeleteFiledException e) {
//            e.printStackTrace();
            log.warn(e.getMessage());
        }
        if (updateSql == null) {
            return statement.toString();
        }
        return updateSql;

    }

    private void updateAppendDeleteField(Update statement) {
        Expression where = statement.getWhere();
        if (MysqlTableFieldCacheUtils.existsFieldInTable(statement.getTable().getName(), fieldName)) {
            if (where != null) {
                AndExpression expression = new AndExpression();
                expression.withLeftExpression(where);
                expression.withRightExpression(new EqualsTo(new Column(fieldName), new LongValue(notDeleteValue)));
                statement.setWhere(expression);
            } else {
                statement.withWhere(new EqualsTo(new Column(fieldName), new LongValue(notDeleteValue)));
            }
        }
    }

    private void selectAppendDeleteField(Select statement) {
        if (ObjectUtil.isNotNull(statement)) {
            parseSelectSql(statement.getSelectBody());
        }
    }

    private void parseSelectSql(SelectBody body) {
        if (body instanceof PlainSelect) {
            PlainSelect plainSelect = (PlainSelect) body;
            FromItem fromItem = plainSelect.getFromItem();
            if (fromItem instanceof Table) {
                tableFieldAppend(plainSelect);
                joinOnDeleteAppend(plainSelect);
            } else if (fromItem instanceof SubSelect) {
                SubSelect subSelect = (SubSelect) fromItem;
                parseSelectSql(subSelect.getSelectBody());
            }

        }
    }

    private void joinOnDeleteAppend(PlainSelect plainSelect) {
        if (plainSelect.getJoins() == null) {
            return;
        }
        for (Join selectJoin : plainSelect.getJoins()) {
            joinTableFieldAppend(selectJoin);
        }
    }

    private void joinTableFieldAppend(Join selectJoin) {
        FromItem rightItem = selectJoin.getRightItem();
        if (rightItem instanceof Table && MysqlTableFieldCacheUtils.existsFieldInTable(((Table) rightItem).getName(), fieldName)) {
            EqualsTo equals = getNotDeleteValueEqualsTo(rightItem);
            selectJoin.setOnExpressions(selectJoin.getOnExpressions().stream().map(data -> new AndExpression(data, equals)).collect(Collectors.toList()));
        } else if (rightItem instanceof SubSelect) {
            parseSelectSql(((SubSelect) rightItem).getSelectBody());
        }
    }

    /**
     * 获取未删除标识   sql
     *
     * @param rightItem table
     * @return equals sql  例如   del_flag = 0
     */
    private EqualsTo getNotDeleteValueEqualsTo(FromItem rightItem) {
        StringBuilder fieldNameBuilder = new StringBuilder();
        parseTable(fieldNameBuilder, rightItem);
        fieldNameBuilder.append(fieldName);
        return new EqualsTo(new Column(fieldNameBuilder.toString()), new LongValue(notDeleteValue));
    }

    private void tableFieldAppend(PlainSelect plainSelect) {
        if (!existWhereDeleteField(plainSelect)) {
            Expression where = plainSelect.getWhere();
            EqualsTo equals = getNotDeleteValueEqualsTo(plainSelect.getFromItem());
            if (where != null) {
                AndExpression expression = new AndExpression();
                expression.withLeftExpression(where);
                expression.withRightExpression(equals);
                plainSelect.setWhere(expression);
            } else {
                plainSelect.setWhere(equals);
            }
        }
    }

    private boolean existWhereDeleteField(PlainSelect plainSelect) {
        if (!MysqlTableFieldCacheUtils.existsFieldInTable(plainSelect.getFromItem(Table.class).getName(), fieldName)) {
            throw NotFountDeleteFiledException.getInstance(StrFormatter.format("table : {}  field : {}  not found", plainSelect.getFromItem(Table.class).getName(), fieldName));
        }

        return recursiveWhere(plainSelect.getWhere());
    }

    private boolean recursiveWhere(Expression expression) {
        if (expression instanceof AndExpression) {
            AndExpression andExpression = (AndExpression) expression;
            return recursiveWhere(andExpression.getLeftExpression()) || recursiveWhere(andExpression.getLeftExpression());
        } else if (expression instanceof EqualsTo) {
            return recursiveWhere(((EqualsTo) expression).getLeftExpression());
        } else if (expression instanceof OrExpression) {
            OrExpression orExpression = (OrExpression) expression;
            return recursiveWhere(orExpression.getLeftExpression()) || recursiveWhere(orExpression.getRightExpression());
        } else if (expression instanceof Column) {
            Column column = (Column) expression;
            return column.getColumnName().equals(fieldName);
        }
        return false;
    }

    private void parseTable(StringBuilder fieldNameBuilder, FromItem fromItem) {
        if (fromItem instanceof Table) {
            Alias alias = fromItem.getAlias();
            if (alias != null) {
                fieldNameBuilder.append(alias.getName()).append(".");
            }
        }
    }

    private String deleteSqlToUpdate(Delete statement) {
        if (!MysqlTableFieldCacheUtils.existsFieldInTable(statement.getTable().getName(), fieldName)) {
            throw NotFountDeleteFiledException.getInstance(StrFormatter.format("table : {}  field : {}  notfound", statement.getTable().getName(), fieldName));
        }
        Update update = BeanUtils.copyBean(statement, Update.class);
        ArrayList<UpdateSet> list = update.getUpdateSets();
        UpdateSet set = new UpdateSet();
        Alias alias = update.getTable().getAlias();
        StringBuilder delFlag = new StringBuilder();
        if (alias != null && StringUtils.isNotBlank(alias.getName())) {
            delFlag.append(alias.getName()).append(".");
        }
        delFlag.append(fieldName);
        set.add(new Column(delFlag.toString()), new LongValue(deleteValue));
        list.add(set);
        return update.toString();
    }
}
